/*
 * Copyright 2012 - 2014 Benjamin Weiss
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.zjsx.lib.ui.crouton;

import android.annotation.TargetApi;
import android.app.Activity;
import android.content.Context;
import android.content.res.Resources;
import android.os.Build;
import android.os.Handler;
import android.os.Message;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.view.ViewTreeObserver;
import android.view.Window;
import android.view.WindowManager;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
import android.widget.AdapterView;
import android.widget.FrameLayout;
import android.widget.RelativeLayout;
import java.util.Iterator;
import java.util.Queue;
import java.util.concurrent.LinkedBlockingQueue;



/**
 * Manages the lifecycle of {@link Crouton}s.
 */
final class Manager extends Handler {
  private static final class Messages {
    private Messages() { /* no-op */ }

    public static final int DISPLAY_CROUTON = 0xc2007;
    public static final int ADD_CROUTON_TO_VIEW = 0xc20074dd;
    public static final int REMOVE_CROUTON = 0xc2007de1;
  }

  private static Manager INSTANCE;

  private final Queue<Crouton> croutonQueue;

  private Manager() {
    croutonQueue = new LinkedBlockingQueue<Crouton>();
  }

  /**
   * @return The currently used instance of the {@link Manager}.
   */
  static synchronized Manager getInstance() {
    if (null == INSTANCE) {
      INSTANCE = new Manager();
    }

    return INSTANCE;
  }

  /**
   * Inserts a {@link Crouton} to be displayed.
   *
   * @param crouton
   *     The {@link Crouton} to be displayed.
   */
  void add(Crouton crouton) {
    croutonQueue.add(crouton);
    displayCrouton();
  }

  /**
   * Displays the next {@link Crouton} within the queue.
   */
  private void displayCrouton() {
    if (croutonQueue.isEmpty()) {
      return;
    }

    // First peek whether the Crouton has an activity.
    final Crouton currentCrouton = croutonQueue.peek();

    // If the activity is null we poll the Crouton off the queue.
    if (null == currentCrouton.getActivity()) {
      croutonQueue.poll();
    }

    if (!currentCrouton.isShowing()) {
      // Display the Crouton
      sendMessage(currentCrouton, Messages.ADD_CROUTON_TO_VIEW);
      if (null != currentCrouton.getLifecycleCallback()) {
        currentCrouton.getLifecycleCallback().onDisplayed();
      }
    } else {
      sendMessageDelayed(currentCrouton, Messages.DISPLAY_CROUTON, calculateCroutonDuration(currentCrouton));
    }
  }

  private long calculateCroutonDuration(Crouton crouton) {
    long croutonDuration = crouton.getConfiguration().durationInMilliseconds;
    croutonDuration += crouton.getInAnimation().getDuration();
    croutonDuration += crouton.getOutAnimation().getDuration();
    return croutonDuration;
  }

  /**
   * Sends a {@link Crouton} within a {@link Message}.
   *
   * @param crouton
   *     The {@link Crouton} that should be sent.
   * @param messageId
   *     The {@link Message} id.
   */
  private void sendMessage(Crouton crouton, final int messageId) {
    final Message message = obtainMessage(messageId);
    message.obj = crouton;
    sendMessage(message);
  }

  /**
   * Sends a {@link Crouton} within a delayed {@link Message}.
   *
   * @param crouton
   *     The {@link Crouton} that should be sent.
   * @param messageId
   *     The {@link Message} id.
   * @param delay
   *     The delay in milliseconds.
   */
  private void sendMessageDelayed(Crouton crouton, final int messageId, final long delay) {
    Message message = obtainMessage(messageId);
    message.obj = crouton;
    sendMessageDelayed(message, delay);
  }

  /*
   * (non-Javadoc)
   *
   * @see android.os.Handler#handleMessage(android.os.Message)
   */
  @Override
  public void handleMessage(Message message) {
    final Crouton crouton = (Crouton) message.obj;
    if (null == crouton) {
      return;
    }
    switch (message.what) {
      case Messages.DISPLAY_CROUTON: {
        displayCrouton();
        break;
      }

      case Messages.ADD_CROUTON_TO_VIEW: {
        addCroutonToView(crouton);
        break;
      }

      case Messages.REMOVE_CROUTON: {
        removeCrouton(crouton);
        if (null != crouton.getLifecycleCallback()) {
          crouton.getLifecycleCallback().onRemoved();
        }
        break;
      }

      default: {
        super.handleMessage(message);
        break;
      }
    }
  }

  /**
   * Adds a {@link Crouton} to the {@link ViewParent} of it's {@link Activity}.
   *
   * @param crouton
   *     The {@link Crouton} that should be added.
   */
  private void addCroutonToView(final Crouton crouton) {
    // don't add if it is already showing
    if (crouton.isShowing()) {
      return;
    }

    final View croutonView = crouton.getView();
    if (null == croutonView.getParent()) {
      ViewGroup.LayoutParams params = croutonView.getLayoutParams();
      if (null == params) {
        params =
            new ViewGroup.MarginLayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
      }
      // display Crouton in ViewGroup if it has been supplied
      if (null != crouton.getViewGroup()) {
        final ViewGroup croutonViewGroup = crouton.getViewGroup();
        if (shouldAddViewWithoutPosition(croutonViewGroup)) {
          croutonViewGroup.addView(croutonView, params);
        } else {
          croutonViewGroup.addView(croutonView, 0, params);
        }
      } else {
        Activity activity = crouton.getActivity();
        if (null == activity || activity.isFinishing()) {
          return;
        }
        handleTranslucentActionBar((ViewGroup.MarginLayoutParams) params, activity);
        handleActionBarOverlay((ViewGroup.MarginLayoutParams) params, activity);

        activity.addContentView(croutonView, params);
      }
    }

    croutonView.requestLayout(); // This is needed so the animation can use the measured with/height
    ViewTreeObserver observer = croutonView.getViewTreeObserver();
    if (null != observer) {
      observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
        @Override
        @TargetApi(16)
        public void onGlobalLayout() {
          if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
            croutonView.getViewTreeObserver().removeGlobalOnLayoutListener(this);
          } else {
            croutonView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
          }

          if(crouton.getInAnimation() != null) {
            croutonView.startAnimation(crouton.getInAnimation());
            announceForAccessibilityCompat(crouton.getActivity(), crouton.getText());
            if (Configuration.DURATION_INFINITE != crouton.getConfiguration().durationInMilliseconds) {
              sendMessageDelayed(crouton, Messages.REMOVE_CROUTON,
                  crouton.getConfiguration().durationInMilliseconds + crouton.getInAnimation().getDuration());
            }
          }
        }
      });
    }
  }

  private boolean shouldAddViewWithoutPosition(ViewGroup croutonViewGroup) {
    return croutonViewGroup instanceof FrameLayout || croutonViewGroup instanceof AdapterView ||
        croutonViewGroup instanceof RelativeLayout;
  }

  @TargetApi(19)
  private void handleTranslucentActionBar(ViewGroup.MarginLayoutParams params, Activity activity) {
    // Translucent status is only available as of Android 4.4 Kit Kat.
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
      final int flags = activity.getWindow().getAttributes().flags;
      final int translucentStatusFlag = WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS;
      if ((flags & translucentStatusFlag) == translucentStatusFlag) {
        setActionBarMargin(params, activity);
      }
    }
  }

  @TargetApi(11)
  private void handleActionBarOverlay(ViewGroup.MarginLayoutParams params, Activity activity) {
    // ActionBar overlay is only available as of Android 3.0 Honeycomb.
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
      final boolean flags = activity.getWindow().hasFeature(Window.FEATURE_ACTION_BAR_OVERLAY);
      if (flags) {
        setActionBarMargin(params, activity);
      }
    }
  }

  private void setActionBarMargin(ViewGroup.MarginLayoutParams params, Activity activity) {
    final int actionBarContainerId = Resources.getSystem().getIdentifier("action_bar_container", "id", "android");
    final View actionBarContainer = activity.findViewById(actionBarContainerId);
    // The action bar is present: the app is using a Holo theme.
    if (null != actionBarContainer) {
      params.topMargin = actionBarContainer.getBottom();
    }
  }

  /**
   * Removes the {@link Crouton}'s view after it's display
   * durationInMilliseconds.
   *
   * @param crouton
   *     The {@link Crouton} added to a {@link ViewGroup} and should be
   *     removed.
   */
  protected void removeCrouton(Crouton crouton) {
    View croutonView = crouton.getView();
    ViewGroup croutonParentView = (ViewGroup) croutonView.getParent();

    if (null != croutonParentView) {
      croutonView.startAnimation(crouton.getOutAnimation());

      // Remove the Crouton from the queue.
      Crouton removed = croutonQueue.poll();

      // Remove the crouton from the view's parent.
      croutonParentView.removeView(croutonView);
      if (null != removed) {
        removed.detachActivity();
        removed.detachViewGroup();
        if (null != removed.getLifecycleCallback()) {
          removed.getLifecycleCallback().onRemoved();
        }
        removed.detachLifecycleCallback();
      }

      // Send a message to display the next crouton but delay it by the out
      // animation duration to make sure it finishes
      sendMessageDelayed(crouton, Messages.DISPLAY_CROUTON, crouton.getOutAnimation().getDuration());
    }
  }

  /**
   * Removes a {@link Crouton} immediately, even when it's currently being
   * displayed.
   *
   * @param crouton
   *     The {@link Crouton} that should be removed.
   */
  void removeCroutonImmediately(Crouton crouton) {
    // if Crouton has already been displayed then it may not be in the queue (because it was popped).
    // This ensures the displayed Crouton is removed from its parent immediately, whether another instance
    // of it exists in the queue or not.
    // Note: crouton.isShowing() is false here even if it really is showing, as croutonView object in
    // Crouton seems to be out of sync with reality!
    if (null != crouton.getActivity() && null != crouton.getView() && null != crouton.getView().getParent()) {
      ((ViewGroup) crouton.getView().getParent()).removeView(crouton.getView());

      // remove any messages pending for the crouton
      removeAllMessagesForCrouton(crouton);
    }
    // remove any matching croutons from queue
    final Iterator<Crouton> croutonIterator = croutonQueue.iterator();
    while (croutonIterator.hasNext()) {
      final Crouton c = croutonIterator.next();
      if (c.equals(crouton) && (null != c.getActivity())) {
        // remove the crouton from the content view
        removeCroutonFromViewParent(crouton);

        // remove any messages pending for the crouton
        removeAllMessagesForCrouton(c);

        // remove the crouton from the queue
        croutonIterator.remove();

        // we have found our crouton so just break
        break;
      }
    }
  }

  /**
   * Removes all {@link Crouton}s from the queue.
   */
  void clearCroutonQueue() {
    removeAllMessages();

    // remove any views that may already have been added to the activity's
    // content view
    for (Crouton crouton : croutonQueue) {
      removeCroutonFromViewParent(crouton);
    }
    croutonQueue.clear();
  }

  /**
   * Removes all {@link Crouton}s for the provided activity. This will remove
   * crouton from {@link Activity}s content view immediately.
   */
  void clearCroutonsForActivity(Activity activity) {
    Iterator<Crouton> croutonIterator = croutonQueue.iterator();
    while (croutonIterator.hasNext()) {
      Crouton crouton = croutonIterator.next();
      if ((null != crouton.getActivity()) && crouton.getActivity().equals(activity)) {
        // remove the crouton from the content view
        removeCroutonFromViewParent(crouton);

        removeAllMessagesForCrouton(crouton);

        // remove the crouton from the queue
        croutonIterator.remove();
      }
    }
  }

  private void removeCroutonFromViewParent(Crouton crouton) {
    if (crouton.isShowing()) {
      ViewGroup parent = (ViewGroup) crouton.getView().getParent();
      if (null != parent) {
        parent.removeView(crouton.getView());
      }
    }
  }

  private void removeAllMessages() {
    removeMessages(Messages.ADD_CROUTON_TO_VIEW);
    removeMessages(Messages.DISPLAY_CROUTON);
    removeMessages(Messages.REMOVE_CROUTON);
  }

  private void removeAllMessagesForCrouton(Crouton crouton) {
    removeMessages(Messages.ADD_CROUTON_TO_VIEW, crouton);
    removeMessages(Messages.DISPLAY_CROUTON, crouton);
    removeMessages(Messages.REMOVE_CROUTON, crouton);

  }

  /**
   * Generates and dispatches an SDK-specific spoken announcement.
   * <p>
   * For backwards compatibility, we're constructing an event from scratch
   * using the appropriate event type. If your application only targets SDK
   * 16+, you can just call View.announceForAccessibility(CharSequence).
   * </p>
   * <p/>
   * note: AccessibilityManager is only available from API lvl 4.
   * <p/>
   * Adapted from https://http://eyes-free.googlecode.com/files/accessibility_codelab_demos_v2_src.zip
   * via https://github.com/coreform/android-formidable-validation
   *
   * @param context
   *     Used to get {@link AccessibilityManager}
   * @param text
   *     The text to announce.
   */
  public static void announceForAccessibilityCompat(Context context, CharSequence text) {
    if (Build.VERSION.SDK_INT >= 4) {
      AccessibilityManager accessibilityManager = null;
      if (null != context) {
        accessibilityManager = (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
      }
      if (null == accessibilityManager || !accessibilityManager.isEnabled()) {
        return;
      }

      // Prior to SDK 16, announcements could only be made through FOCUSED
      // events. Jelly Bean (SDK 16) added support for speaking text verbatim
      // using the ANNOUNCEMENT event type.
      final int eventType;
      if (Build.VERSION.SDK_INT < 16) {
        eventType = AccessibilityEvent.TYPE_VIEW_FOCUSED;
      } else {
        eventType = AccessibilityEvent.TYPE_ANNOUNCEMENT;
      }

      // Construct an accessibility event with the minimum recommended
      // attributes. An event without a class name or package may be dropped.
      final AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
      event.getText().add(text);
      event.setClassName(Manager.class.getName());
      event.setPackageName(context.getPackageName());

      // Sends the event directly through the accessibility manager. If your
      // application only targets SDK 14+, you should just call
      // getParent().requestSendAccessibilityEvent(this, event);
      accessibilityManager.sendAccessibilityEvent(event);
    }
  }

  @Override
  public String toString() {
    return "Manager{" +
        "croutonQueue=" + croutonQueue +
        '}';
  }
}
